{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# A Long-Term Memory Agent\n",
    "\n",
    "- Author: [byoon](https://github.com/acho98)\n",
    "- Peer Review: \n",
    "- Proofread : [hong-seongmin](https://github.com/hong-seongmin)\n",
    "- This is a part of [LangChain Open Tutorial](https://github.com/LangChain-OpenTutorial/LangChain-OpenTutorial)\n",
    "\n",
    "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/LangChain-OpenTutorial/LangChain-OpenTutorial/blob/main/17-LangGraph/01-Core-Features/17-LangGraph-LongTermMemoryAgent.ipynb)[![Open in GitHub](https://img.shields.io/badge/Open%20in%20GitHub-181717?style=flat-square&logo=github&logoColor=white)](https://github.com/LangChain-OpenTutorial/LangChain-OpenTutorial/blob/main/17-LangGraph/01-Core-Features/17-LangGraph-LongTermMemoryAgent.ipynb)\n",
    "## Overview\n",
    "This tutorial explains how to implement an agent with long-term memory capabilities using LangGraph. The agent can store, retrieve, and use memories to enhance its interactions with users.\n",
    "\n",
    "Inspired by Research\n",
    "The concept of long-term memory in agents is inspired by papers like [MemGPT](https://memgpt.ai/) and is based on our own work. The agent extracts memories from chat interactions and stores them in a database.\n",
    "\n",
    "Memory Representation\n",
    "In this tutorial, \"memory\" is represented in two ways:\n",
    "\n",
    "- Text Information: A piece of text generated by the agent.\n",
    "- Structured Information: Knowledge about entities extracted by the agent, represented as (subject, predicate, object) knowledge triples.   \n",
    "This structured memory can be read or queried semantically to provide personalized context during interactions with users.\n",
    "\n",
    "Key Idea: Shared Memory Across Conversations   \n",
    "The KEY idea is that by saving memories, the agent persists information about users that is SHARED across multiple conversations (threads), which is different from memory of a single conversation that is already enabled by LangGraph's [persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/).\n",
    "\n",
    "<img src=\"./assets/17-long-term-mem-agent.png\" width=\"500\">\n",
    "\n",
    "\n",
    "By using long-term memory, the agent can provide more personalized and context-aware responses across different conversations.\n",
    "\n",
    "### Table of Contents\n",
    "\n",
    "- [Overview](#overview)\n",
    "- [Environement Setup](#environment-setup)\n",
    "- [Defne vectorstore for memories](#define-vectorstore-for-memories)\n",
    "- [Define state, nodes and edges](#define-state-nodes-and-edges)\n",
    "- [Build the graph](#build-the-graph)\n",
    "- [Run the agent](#run-the-agent)\n",
    "- [Adding structed memories](#adding-structured-memories)\n",
    "\n",
    "\n",
    "### References\n",
    "- [LangGraph Persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/#checkpoints)\n",
    "- [Lang-memgpt](https://github.com/langchain-ai/lang-memgpt)\n",
    "- [InMemoryByteStore](https://python.langchain.com/api_reference/core/stores/langchain_core.stores.InMemoryByteStore.html)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Environment-setup\n",
    "\n",
    "Set up the environment. You may refer to [Environment Setup](https://wikidocs.net/257836) for more details.\n",
    "\n",
    "**[Note]**\n",
    "- ```langchain-opentutorial``` is a package that provides a set of easy-to-use environment setup, useful functions and utilities for tutorials. \n",
    "- You can checkout the [```langchain-opentutorial```](https://github.com/LangChain-OpenTutorial/langchain-opentutorial-pypi) for more details."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%capture --no-stderr\n",
    "%pip install langchain-opentutorial"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Install required packages\n",
    "from langchain_opentutorial import package\n",
    "\n",
    "package.install(\n",
    "    [\n",
    "        \"langchain\",\n",
    "        \"langchain_community\",\n",
    "        \"langchain_openai\",\n",
    "        \"langgraph\",\n",
    "        \"tiktoken\",\n",
    "        \"matplotlib\",\n",
    "        \"networkx\",\n",
    "    ],\n",
    "    verbose=False,\n",
    "    upgrade=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Environment variables have been set successfully.\n"
     ]
    }
   ],
   "source": [
    "# Set environment variables\n",
    "from langchain_opentutorial import set_env\n",
    "\n",
    "set_env(\n",
    "    {\n",
    "        \"OPENAI_API_KEY\": \"\",\n",
    "        \"TAVILY_API_KEY\": \"\",\n",
    "    },\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from dotenv import load_dotenv\n",
    "\n",
    "load_dotenv(override=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import List\n",
    "import uuid\n",
    "import tiktoken\n",
    "from langchain_community.tools.tavily_search import TavilySearchResults\n",
    "from langchain_core.documents import Document\n",
    "from langchain_core.messages import get_buffer_string\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "from langchain_core.tools import tool\n",
    "from langchain_core.vectorstores import InMemoryVectorStore\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_openai.embeddings import OpenAIEmbeddings\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "from langgraph.graph import END, START, MessagesState, StateGraph\n",
    "from langgraph.prebuilt import ToolNode"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Define vectorstore for memories\n",
    "\n",
    "We will define the vectorstore where our memories will be stored and retrieved based on the conversation context.   \n",
    "Memories will be stored as embeddings and later looked up when needed.\n",
    "In this tutorial, we will use an in-memory vectorstore to manage and store our memories efficiently."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "recall_vector_store = InMemoryVectorStore(OpenAIEmbeddings())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Define tools\n",
    "\n",
    "Next, we will define our memory tools.  \n",
    "We need one tool to store memories and another tool to search for the most relevant memory based on the conversation context."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_user_id(config: RunnableConfig) -> str:\n",
    "    user_id = config[\"configurable\"].get(\"user_id\")\n",
    "    if user_id is None:\n",
    "        raise ValueError(\"User ID needs to be provided to save a memory.\")\n",
    "    return user_id\n",
    "\n",
    "@tool\n",
    "def save_recall_memory(memory: str, config: RunnableConfig) -> str:\n",
    "    \"\"\"Save memory to vectorstore for later semantic retrieval.\"\"\"\n",
    "    user_id = get_user_id(config)\n",
    "    document = Document(\n",
    "        page_content=memory, id=str(uuid.uuid4()), metadata={\"user_id\": user_id}\n",
    "    )\n",
    "    recall_vector_store.add_documents([document])\n",
    "    return memory\n",
    "\n",
    "@tool\n",
    "def search_recall_memories(query: str, config: RunnableConfig) -> List[str]:\n",
    "    \"\"\"Search for relevant memories.\"\"\"\n",
    "    user_id = get_user_id(config)\n",
    "\n",
    "    def _filter_function(doc: Document) -> bool:\n",
    "        return doc.metadata.get(\"user_id\") == user_id\n",
    "\n",
    "    documents = recall_vector_store.similarity_search(\n",
    "        query, k=3, filter=_filter_function\n",
    "    )\n",
    "    return [document.page_content for document in documents]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Additionally, let's give our agent ability to search the web using Tavily."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "search = TavilySearchResults(max_results=1)\n",
    "tools = [save_recall_memory, search_recall_memories, search]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Define state, nodes and edges\n",
    "\n",
    "Our graph state consists of two channels:\n",
    "\n",
    "- Messages: Stores the chat history to maintain conversation context.\n",
    "- Recall Memories: Holds contextual memories that are retrieved before calling the agent and included in the system prompt.\n",
    "\n",
    "This structure ensures relevant information is available for better responses.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "class State(MessagesState):\n",
    "    # add memories that will be retrieved based on the conversation context\n",
    "    recall_memories: List[str]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define the prompt template for the agent\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\n",
    "            \"system\",\n",
    "            \"You are a helpful assistant with advanced long-term memory\"\n",
    "            \" capabilities. Powered by a stateless LLM, you must rely on\"\n",
    "            \" external memory to store information between conversations.\"\n",
    "            \" Utilize the available memory tools to store and retrieve\"\n",
    "            \" important details that will help you better attend to the user's\"\n",
    "            \" needs and understand their context.\\n\\n\"\n",
    "            \"Memory Usage Guidelines:\\n\"\n",
    "            \"1. Actively use memory tools (save_recall_memory)\"\n",
    "            \" to build a comprehensive understanding of the user.\\n\"\n",
    "            \"2. Make informed suppositions and extrapolations based on stored\"\n",
    "            \" memories.\\n\"\n",
    "            \"3. Regularly reflect on past interactions to identify patterns and\"\n",
    "            \" preferences.\\n\"\n",
    "            \"4. Update your mental model of the user with each new piece of\"\n",
    "            \" information.\\n\"\n",
    "            \"5. Cross-reference new information with existing memories for\"\n",
    "            \" consistency.\\n\"\n",
    "            \"6. Prioritize storing emotional context and personal values\"\n",
    "            \" alongside facts.\\n\"\n",
    "            \"7. Use memory to anticipate needs and tailor responses to the\"\n",
    "            \" user's style.\\n\"\n",
    "            \"8. Recognize and acknowledge changes in the user's situation or\"\n",
    "            \" perspectives over time.\\n\"\n",
    "            \"9. Leverage memories to provide personalized examples and\"\n",
    "            \" analogies.\\n\"\n",
    "            \"10. Recall past challenges or successes to inform current\"\n",
    "            \" problem-solving.\\n\\n\"\n",
    "            \"## Recall Memories\\n\"\n",
    "            \"Recall memories are contextually retrieved based on the current\"\n",
    "            \" conversation:\\n{recall_memories}\\n\\n\"\n",
    "            \"## Instructions\\n\"\n",
    "            \"Engage with the user naturally, as a trusted colleague or friend.\"\n",
    "            \" There's no need to explicitly mention your memory capabilities.\"\n",
    "            \" Instead, seamlessly incorporate your understanding of the user\"\n",
    "            \" into your responses. Be attentive to subtle cues and underlying\"\n",
    "            \" emotions. Adapt your communication style to match the user's\"\n",
    "            \" preferences and current emotional state. Use tools to persist\"\n",
    "            \" information you want to retain in the next conversation. If you\"\n",
    "            \" do call tools, all text preceding the tool call is an internal\"\n",
    "            \" message. Respond AFTER calling the tool, once you have\"\n",
    "            \" confirmation that the tool completed successfully.\\n\\n\",\n",
    "        ),\n",
    "        (\"placeholder\", \"{messages}\"),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The purpose of each function is as follows:\n",
    "- ```agent()```: Generates a response using GPT-4o with recalled memories and tool integration.\n",
    "- ```load_memories()```: Retrieves relevant past memories based on the conversation history.\n",
    "- ```route_tools()```: Determines whether to use tools or end the conversation based on the last message."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = ChatOpenAI(model_name=\"gpt-4o\")\n",
    "model_with_tools = model.bind_tools(tools)\n",
    "tokenizer = tiktoken.encoding_for_model(\"gpt-4o\")\n",
    "\n",
    "def agent(state: State) -> State:\n",
    "    \"\"\"Process the current state and generate a response using the LLM.\n",
    "\n",
    "    Args:\n",
    "        state (schemas.State): The current state of the conversation.\n",
    "\n",
    "    Returns:\n",
    "        schemas.State: The updated state with the agent's response.\n",
    "    \"\"\"\n",
    "    bound = prompt | model_with_tools\n",
    "    recall_str = (\n",
    "        \"<recall_memory>\\n\" + \"\\n\".join(state[\"recall_memories\"]) + \"\\n</recall_memory>\"\n",
    "    )\n",
    "    prediction = bound.invoke(\n",
    "        {\n",
    "            \"messages\": state[\"messages\"],\n",
    "            \"recall_memories\": recall_str,\n",
    "        }\n",
    "    )\n",
    "    return {\n",
    "        \"messages\": [prediction],\n",
    "    }\n",
    "\n",
    "def load_memories(state: State, config: RunnableConfig) -> State:\n",
    "    \"\"\"Load memories for the current conversation.\n",
    "\n",
    "    Args:\n",
    "        state (schemas.State): The current state of the conversation.\n",
    "        config (RunnableConfig): The runtime configuration for the agent.\n",
    "\n",
    "    Returns:\n",
    "        State: The updated state with loaded memories.\n",
    "    \"\"\"\n",
    "    convo_str = get_buffer_string(state[\"messages\"])\n",
    "    convo_str = tokenizer.decode(tokenizer.encode(convo_str)[:2048])\n",
    "    recall_memories = search_recall_memories.invoke(convo_str, config)\n",
    "    return {\n",
    "        \"recall_memories\": recall_memories,\n",
    "    }\n",
    "\n",
    "def route_tools(state: State):\n",
    "    \"\"\"Determine whether to use tools or end the conversation based on the last message.\n",
    "\n",
    "    Args:\n",
    "        state (schemas.State): The current state of the conversation.\n",
    "\n",
    "    Returns:\n",
    "        Literal[\"tools\", \"__end__\"]: The next step in the graph.\n",
    "    \"\"\"\n",
    "    msg = state[\"messages\"][-1]\n",
    "    if msg.tool_calls:\n",
    "        return \"tools\"\n",
    "\n",
    "    return END"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Build the graph\n",
    "\n",
    "Our agent graph will be very similar to a simple ReAct agent.  \n",
    "The only key difference is that we add a node to load memories before calling the agent for the first time."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create the graph and add nodes\n",
    "builder = StateGraph(State)\n",
    "builder.add_node(load_memories)\n",
    "builder.add_node(agent)\n",
    "builder.add_node(\"tools\", ToolNode(tools))\n",
    "\n",
    "# Add edges to the graph\n",
    "builder.add_edge(START, \"load_memories\")\n",
    "builder.add_edge(\"load_memories\", \"agent\")\n",
    "builder.add_conditional_edges(\"agent\", route_tools, [\"tools\", END])\n",
    "builder.add_edge(\"tools\", \"agent\")\n",
    "\n",
    "# Compile the graph\n",
    "memory = MemorySaver()\n",
    "graph = builder.compile(checkpointer=memory)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Run the agent\n",
    "Let's run the agent for the first time and tell it some information about the user."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "def pretty_print_stream_chunk(chunk):\n",
    "    for node, updates in chunk.items():\n",
    "        print(f\"Update from node: {node}\")\n",
    "        if \"messages\" in updates:\n",
    "            updates[\"messages\"][-1].pretty_print()\n",
    "        else:\n",
    "            print(updates)\n",
    "        print(\"\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note: we're specifying ```user_id``` to save memories for a given user."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Update from node: load_memories\n",
      "{'recall_memories': []}\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "Nice to meet you, Polar Bear! How can I assist you today?\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "config = {\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"1\"}}\n",
    "\n",
    "for chunk in graph.stream({\"messages\": [(\"user\", \"my name is Polar bear\")]}, config=config):\n",
    "    pretty_print_stream_chunk(chunk)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can see that the agent saved the memory about user's name.  \n",
    "Let's add some more information about the user."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Update from node: load_memories\n",
      "{'recall_memories': []}\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  save_recall_memory (call_kcUeAsdkjyeEGo7kukZoRUMe)\n",
      " Call ID: call_kcUeAsdkjyeEGo7kukZoRUMe\n",
      "  Args:\n",
      "    memory: Polar Bear loves pizza.\n",
      "\n",
      "\n",
      "Update from node: tools\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: save_recall_memory\n",
      "\n",
      "Polar Bear loves pizza.\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "That's great to hear! Pizza is a delicious choice. Do you have a favorite type of pizza or any toppings you particularly enjoy?\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "for chunk in graph.stream({\"messages\": [(\"user\", \"i love pizza\")]}, config=config):\n",
    "    pretty_print_stream_chunk(chunk)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Update from node: load_memories\n",
      "{'recall_memories': ['Polar Bear loves pizza.']}\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  save_recall_memory (call_mdtowGoUTb7N75AI4eJgQvcs)\n",
      " Call ID: call_mdtowGoUTb7N75AI4eJgQvcs\n",
      "  Args:\n",
      "    memory: Polar Bear's favorite pizza topping is pepperoni.\n",
      "\n",
      "\n",
      "Update from node: tools\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: save_recall_memory\n",
      "\n",
      "Polar Bear's favorite pizza topping is pepperoni.\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "Pepperoni is a classic and tasty choice! Do you have a favorite place to get pizza from, or do you enjoy making it at home?\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "for chunk in graph.stream(\n",
    "    {\"messages\": [(\"user\", \"pepperoni!\")]},\n",
    "    config={\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"1\"}},\n",
    "):\n",
    "    pretty_print_stream_chunk(chunk)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Update from node: load_memories\n",
      "{'recall_memories': ['Polar Bear loves pizza.', \"Polar Bear's favorite pizza topping is pepperoni.\"]}\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  save_recall_memory (call_JxW02FHio010cKkgUAfm62Dg)\n",
      " Call ID: call_JxW02FHio010cKkgUAfm62Dg\n",
      "  Args:\n",
      "    memory: Polar Bear recently moved to New York.\n",
      "\n",
      "\n",
      "Update from node: tools\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: save_recall_memory\n",
      "\n",
      "Polar Bear recently moved to New York.\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "That's exciting! New York is famous for its amazing pizza. Have you had a chance to explore any of the local pizzerias yet?\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "for chunk in graph.stream(\n",
    "    {\"messages\": [(\"user\", \"i also just moved to new york\")]},\n",
    "    config={\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"1\"}},\n",
    "):\n",
    "    pretty_print_stream_chunk(chunk)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we can use the saved information about our user on a different thread. Let's try it out:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Update from node: load_memories\n",
      "{'recall_memories': ['Polar Bear loves pizza.', \"Polar Bear's favorite pizza topping is pepperoni.\", 'Polar Bear recently moved to New York.']}\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "Considering you're in New York and have a love for pizza, why not try out some of the city's iconic pizzerias? You might enjoy a visit to places like Di Fara Pizza, Prince Street Pizza, or Joe's Pizza. They’re known for their delicious pepperoni pizzas, which I remember is your favorite topping! Does that sound like a plan, or are you in the mood for something different tonight?\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "config = {\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"2\"}}\n",
    "\n",
    "for chunk in graph.stream(\n",
    "    {\"messages\": [(\"user\", \"where should i go for dinner?\")]}, config=config\n",
    "):\n",
    "    pretty_print_stream_chunk(chunk)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "Notice that the agent loads the most relevant memories before responding.  \n",
    "In our case, it suggests dinner recommendations based on both food preferences and location.\n",
    "\n",
    "Finally, let's use the search tool along with the conversation history and memory to find the location of a pizzeria."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Update from node: load_memories\n",
      "{'recall_memories': ['Polar Bear loves pizza.', \"Polar Bear's favorite pizza topping is pepperoni.\", 'Polar Bear recently moved to New York.']}\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  tavily_search_results_json (call_PoUJZ0mWgZjqoiiQsI52uqrS)\n",
      " Call ID: call_PoUJZ0mWgZjqoiiQsI52uqrS\n",
      "  Args:\n",
      "    query: Joe's Pizza Greenwich Village address\n",
      "\n",
      "\n",
      "Update from node: tools\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: tavily_search_results_json\n",
      "\n",
      "[{\"url\": \"https://www.joespizzanyc.com/\", \"content\": \"Established in 1975 by Joe Pozzuoli, who is originally from Naples, Italy, the birthplace of pizza, Joe's Pizza is a \\\"Greenwich Village institution\\\" offering the classic New York slice for over 47 years. First, we served our customers from our corner location at Bleecker and Carmine Street and now three doors down at 7 Carmine Street. At 75\"}]\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "Joe's Pizza in Greenwich Village is located at 7 Carmine Street, New York, NY. Enjoy your pepperoni pizza there!\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "for chunk in graph.stream(\n",
    "    {\"messages\": [(\"user\", \"what's the address for joe's in greenwich village?\")]},\n",
    "    config=config,\n",
    "):\n",
    "    pretty_print_stream_chunk(chunk)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Adding structured memories\n",
    "\n",
    "So far, we have stored memories as simple text, like \"John loves pizza\". This format works well when saving memories in a vector database.\n",
    "\n",
    "However, if your application would benefit from other storage options—such as a graph database—we can modify the system to store memories in a more structured way.\n",
    "\n",
    "Below, we update the ```save_recall_memory``` tool to accept a list of \"knowledge triples\" (3-part structures with a subject, predicate, and object), which can be stored in a knowledge graph. The model will then generate these structured representations when using its tools.\n",
    "\n",
    "For now, we continue using the same vector database, but ```save_recall_memory``` and ```search_recall_memories``` can be further modified to work with a graph database.  \n",
    "At this stage, we only need to update the ```save_recall_memory``` tool."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "recall_vector_store = InMemoryVectorStore(OpenAIEmbeddings())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing_extensions import TypedDict\n",
    "\n",
    "class KnowledgeTriple(TypedDict):\n",
    "    subject: str\n",
    "    predicate: str\n",
    "    object_: str\n",
    "\n",
    "@tool\n",
    "def save_recall_memory(memories: List[KnowledgeTriple], config: RunnableConfig) -> str:\n",
    "    \"\"\"Save memory to vectorstore for later semantic retrieval.\"\"\"\n",
    "    user_id = get_user_id(config)\n",
    "    for memory in memories:\n",
    "        serialized = \" \".join(memory.values())\n",
    "        document = Document(\n",
    "            serialized,\n",
    "            id=str(uuid.uuid4()),\n",
    "            metadata={\n",
    "                \"user_id\": user_id,\n",
    "                **memory,\n",
    "            },\n",
    "        )\n",
    "        recall_vector_store.add_documents([document])\n",
    "    return memories"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can then compile the graph exactly as before:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [],
   "source": [
    "tools = [save_recall_memory, search_recall_memories, search]\n",
    "model_with_tools = model.bind_tools(tools)\n",
    "\n",
    "# Create the graph and add nodes\n",
    "builder = StateGraph(State)\n",
    "builder.add_node(load_memories)\n",
    "builder.add_node(agent)\n",
    "builder.add_node(\"tools\", ToolNode(tools))\n",
    "\n",
    "# Add edges to the graph\n",
    "builder.add_edge(START, \"load_memories\")\n",
    "builder.add_edge(\"load_memories\", \"agent\")\n",
    "builder.add_conditional_edges(\"agent\", route_tools, [\"tools\", END])\n",
    "builder.add_edge(\"tools\", \"agent\")\n",
    "\n",
    "# Compile the graph\n",
    "memory = MemorySaver()\n",
    "graph = builder.compile(checkpointer=memory)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Update from node: load_memories\n",
      "{'recall_memories': []}\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "Hello, Sasako! How can I assist you today?\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "config = {\"configurable\": {\"user_id\": \"3\", \"thread_id\": \"1\"}}\n",
    "\n",
    "for chunk in graph.stream({\"messages\": [(\"user\", \"Hi, I'm Sasako.\")]}, config=config):\n",
    "    pretty_print_stream_chunk(chunk)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Update from node: load_memories\n",
      "{'recall_memories': []}\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  save_recall_memory (call_40xET1DW4j6CfjEUq8VzbPpP)\n",
      " Call ID: call_40xET1DW4j6CfjEUq8VzbPpP\n",
      "  Args:\n",
      "    memories: [{'subject': 'Sasako', 'predicate': 'has a friend who likes', 'object_': 'Pizza'}, {'subject': 'Sasako', 'predicate': \"friend's name is\", 'object_': 'Polar bear'}]\n",
      "\n",
      "\n",
      "Update from node: tools\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: save_recall_memory\n",
      "\n",
      "[{\"subject\": \"Sasako\", \"predicate\": \"has a friend who likes\", \"object_\": \"Pizza\"}, {\"subject\": \"Sasako\", \"predicate\": \"friend's name is\", \"object_\": \"Polar bear\"}]\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "That's great to know, Sasako! Pizza is a popular favorite. Is there anything else you'd like to share or ask about?\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "for chunk in graph.stream(\n",
    "    {\"messages\": [(\"user\", \"My friend Polar bear likes Pizza.\")]}, config=config\n",
    "):\n",
    "    pretty_print_stream_chunk(chunk)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As before, the memories generated from one thread are accessed in another thread from the same user:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Update from node: load_memories\n",
      "{'recall_memories': [\"Sasako friend's name is Polar bear\", 'Sasako has a friend who likes Pizza']}\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "Since Polar bear likes pizza, bringing a variety of pizzas could be a great idea for the party. You might also consider bringing some complementary sides like garlic bread, salad, or a selection of drinks to go with the pizzas.\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "config = {\"configurable\": {\"user_id\": \"3\", \"thread_id\": \"2\"}}\n",
    "\n",
    "for chunk in graph.stream(\n",
    "    {\"messages\": [(\"user\", \"What food should I bring to Polar bear's party?\")]}, config=config\n",
    "):\n",
    "    pretty_print_stream_chunk(chunk)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For illustrative purposes we can visualize the knowledge graph extracted by the model:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAe8AAAFPCAYAAABklUYjAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAMTgAADE4Bf3eMIwAAN0hJREFUeJzt3Xd4k+X+x/H3k7RpS3fZLSqCTAcqiCDiFtwLD4p7ISiIIO6tB48HtyIKHvfeP44iKCiCehRQERWQIVqwZbeli6Rpkuf3x00oIMrKzud1XVxt0ifJ/egFn37vadm2bSMiIiJxwxHtBoiIiMjOUXiLiIjEGYW3iIhInFF4i4iIxBmFt4iISJxReIuIiMQZhbeIiEicUXiLiIjEGYW3iIhInFF4i4iIxBmFt4iISJxReIuIiMQZhbeIiEicUXiLiIjEGYW3iIhInFF4i4iIxJmUaDdAREQkXPwBmypvPVV1PnwBG79tE7DBYYHTskhxWOSkpZDjSsXpsKLd3B2m8BYRkYRRWVdPmdtLhdt8ra33AyasAezNrg1GdWDjk5mpThpnuMjPSKVxhovctNSItXtnWbZt29u/TEREJDb5AjYl1W6WlNdS4/XhsCz8uxFtTssiYNtkuVJoV5DJHtkZMVeVK7xFRCQu1Xh9LK2opbjSDdj4w5BmTgvAonVuBm3zM8lyxUaHtcJbRETiitcfYO7qSkqrPVhWQ7d3ODkssG1olZ1Ol+a5uJzRne+t8BYRkbixssbD9yvX49s48SzSHBakWBbdWubRIis98g3YSOEtIiIxb/NqOxZCyyK6VbjCW0REYtq6DV5mlpZHrdr+K8EqvEdRAU0auSL62QpvERGJWatqPMxcURFTob01hwU9CvMj2o2u8BYRkZhUWu1m9or1MdFNvj0W0L0wj6LsjIh8nrZHFRGRmLOqxhM3wQ1m85fZK9azqsYTkc9TeIuISEwxY9wVcRPcQTYwc0UF6zZ4w/5ZCm8REYkZXn+AmaXlBKLdkF0UsGFmaTlef3jvQOEtIiIxY+7qSnxxPhXLZ9v8uLoyrJ+h8BYRkZiwssZDabUnpmeW74iADSXVnrCOfyu8RUQk6rz+AN+vjJ8JattjA9+tXB+27nOFt4iIRF0idJdvLZzd5wpvERGJqhqvLyG6y7cW7D6v8fpC/t4KbxERiaqlFbVYsXVcdshYlrm/UFN4i4hI1PgCNsWV7oSruoMCNhRXuvGH+AYV3iIiEjUl1W5ImGlqf8XeeJ+ho/AWEZGoWVJeiz/Bs9tvw+Ly0HadK7xFRCQqKuvqwzKZKxbVeH1U1tWH7P0U3iIiEhVlbi+ORJ2pthWHZVHuVniLiEicq3DX40+wtd1/xW/blLtDd2CJwltERKKiLIRhFg/KPApvERGJY/6ATW29P9rNiKharz9kS8YU3iIiEnEpTgdndSykX8dCztl/L4b07cUHL4zfoddOe/8t+nUsZMzNw0PWnjsv7Ee/joXMm/V1yN5zW6pCNEFP4S0iIlFzzegnuPy2UXhqa3hp9D38b/IHYf9Mvy86M9wdFlTV1eMLwecrvEVEJGoOO+EU+px7IUec1g+AX76bBUDxwvnce/kALureiUt77sfooZexannxNt/jm08+YkjfXgzo0oYLurbn1nNPZeGc2QCsKfmDfh0LGXhkV8bdeSMXH7ovMz547y/b88NXn3P18T25qHsnnv3nbZuCfk1pCQ8NH8RlvQ7gwkM6Mmrg+ZQsXQLAquXFXH/m8Zx/cDsGdGnDNSf25tN3X9/0noOP6U6/joW8+MA/6bV/JwYOHLjb/91SdvsdREREdlH1+gq8Hg/zZv4PgGZFe1BbVck/rziPqopyzrnmeuo2bOD9Z8ZQsnQJj34w7U/vkZmTQ99zL6JRVhbla1bz4QvjeXj4YP7zxZxN15SvXklVRRkX3Xg7rTt2/sv2/PDF55w5cCgfv/4Ck197gcK929J3wMXcf9XFrP6jmBPPv5S09Aw+euV57rvyAp6Y/CUOZwo9+pxMXtNm1FZWMu39Nxl/5410Org7RW322fTec7+awVUjb+DAffbe7f9uCm8REYmaK4/suun7Lr2OpO95FzNv1tesX7eWLocdwdmDrwXgu8+nsnzJQpYvWfin9/BsqOXj119k9R/LNj23oaaa9evWbnqclpHBiIefItWV9rft6T/kOnr0OYnsvHweHHYFc7+czgE9e7N88S8ATHj2qU3X1lRW8Mevi0hLz+D76Z/x688/EAg0nN9dvHD+FuE98M77OKvvcXRsnLWD/3X+msJbRESi5rZnXiUtoxFNC1vRrKjVX1/4N5u5PHP3LVSsXc3FN97JXh0789TtI1m3ohSvx7Ppmpz8xtsN7u1pUljE1aMe3vTYDgRoVrQH/7n3Vhb/+D1Hn9mfw08+g8mvvcB3n0+lbrPPB2jSohBniPakUXiLiEjU7HfoYbjS0rd4ruNB3chr0pR5s7/m/fFj8Lg3sHzxLxTu3ZY923WkeOGCbb5XdeV6fv7mK9atKN3l9rw99hFqKiuY/NoLABzY+yhatm7Dnu06snzJQmZNncw++x/ImpLlfPHh+zw19ZtNr91QU03pb7/yy/ez//L9Q7WjnCasiYhITMnMyeWOZ19nv+6HMeG5p5jy5ssccmxfbhv/CimpqX+6ftA9/6ZJy0ImvfIstdVV7N1p313+7IN6H8UHL4xn7YpSTjz/UvqeexFOp5Nbxr1Mr5NOZ9bUSfznnlv48qMJHNCzNwDnDB1J230PYM6MaXw7bQpdjzruL98/xRGa8LZsO0n2phMRkZhR4fEyfVlZwh8GujkLOGqvJuSn//kXkJ2lyltERCIux7X7ARaPclyhGa1WeIuISMQ5HRaZqc5oNyOiMl1OnCHqNld4i4hIVDTOcEW7CRHVOD1096vwFhGRqMjPSMWZJOd5Oy2LghD+sqLwFhGRqGic4SKQJHOmA7ZNQUboxvkV3iIiEhW5aalkhWgCV6zLcqWQm6bwFhGRBNCuIDNku47FKqcF7QsyQ/qeCm8REYmaVtkZmBXQiczaeJ+ho/AWEZGoSXFYtM7NIEQrqGKOw4LWuRkhWyK26X1D+m4iIiI7qW1+Jok6b822zf2FmsJbRESiKsuVQqvs9ISrvh0WtMpOD8ukPIW3iIhETGlpKQMHDuTEE0+kW7du7LnnnrhcLq6/oB8pCbbmO8Wy6NI8NzzvHZZ3FRER2Ya6ujpeeOEF/H7/Fs8PGTyYri3zmFlakRCHlVhAt5Z5uJzhqZFVeYuISMS0adOGfv36YW2sslNTUzn//PPp378/LbPSKUqA7vNgd3mLrPTtX7yrnxG2dxYREdlMaWkpAwYMYNKkSTRq1AiARo0a8cQTT2y65sDmuXHffR7O7vIghbeIiISV1+vlwQcfpGPHjjgcDhYtWsTzzz8PwPjx4ykoKNh0rcvpoEdRQdxW3w4LehQVhK27PMiy7USdoC8iItH22WefMXToUFJSUnjyySc58sgjAbBtm9mzZ9O9e/dNXeibW1Xj4Zs4G/+2gJ5F+WHtLt/0WQpvEREJtZKSEq677jo+/vhj7r33XoYMGUJq6s7t7V1a7Wb2ivVxEeAW0L0wj6IQ76T2V9RtLiIiIeP1ehk9ejSdOnUiLS2NRYsWMXz48J0OboCi7Ax6FuXHfBe6w4KerfIjFtygyltEREJk6tSpXHPNNbhcLsaOHUvv3r1D8r7rNniZWVqOz7YJxFBiOSwzOa1HUQFNGoXurO4dofAWEZHdsnz5cq677jqmTp26qYs8JSW024h4/QHmrq6ktNoTE93oFmY5WJfmuWGfnLYt6jYXEZFdUldXx/3330/nzp3JzMxk0aJFXHvttSEPbjCz0LsX5tOjKB+Xw4paV7rDApfDomdRPocU5kcluEGVt4iI7IJPPvmEa665hkaNGjF27Fh69eoVsc/2+gP8uLqSkmoPlkVEutIdljlkJJrV9uYU3iIissOWLVvGiBEjmDZtGqNGjWLw4MFhqbR3RI3Xx9KKWoor3YCNPwxp5rQALPbOy6BNXmZYDhnZFbHRChERiWkej4eHHnqI+++/n3POOYfFixfTrFmzqLYpy5VCl+a57Nc0h5JqN4vLa6nx+nBYFv7dqEudlkXAtslypdC+IJNW2aE/j3t3KbxFRORvTZ48mWHDhpGdnc2nn35Kz549o92kLTgdFnvlNmKv3EZU1tVT7q6n3O2lzO2ltt4cgBLM3s0jPRjHwW73TJeTxukuCjJcFGSkkpu288vbIkXd5iIisk3FxcUMHz6cGTNmcN999zFo0CCcTme0m7VT/AGbKq+Pqrp6fAGbgG26150WOCyLFIdFTloqOa6UmKuu/44qbxER2YLH4+GBBx5g9OjRDBgwgMWLF9O0adNoN2uXOB0W+emp5KfHbhW9KxTeIiKyycSJE7n22mvJz89n2rRpHHroodFukmyDus1FRJKF3w9/0e1t2zbz5s3jyCOP5P777+eKK66Iuy7yZKLwFhFJdFuH9g8/wEEH/emy+vp63G43OTk5EWyc7ArtsCYikqjKyszXYHC/8w4ccABMmQLV1X+6PDU1VcEdJzTmLSKSaGwbXn8dKith8GBwOGDsWHjjDfP8fvtFu4Wym9RtLiKSiEpLoagI5s0zYX3jjbD33tCyJVRUwIIFsMceMGxYtFsqu0Dd5iIiiaiw0FTbt94Ka9ZAr17wySfw/vumOz0rC156CX75JdotlV2gbnMRkURkWdC9O/z8M7z4oqm8TzoJUjeud5440fysVauoNlN2jSpvEZF4FQj8+bnNR0IPOQS6dTMhPXWqCe7p06F/f3joIRg0CLKzI9ZcCR2Ft4hIPLJtMxEN4L//hY8+MhPUrI1bfPrNnt6ceKIZ5540CdxuMxZ+xBEmxI8/PipNl92nCWsiIvFqyRLTHR4ImMlpv/8O990HBx+85XXvvw+vvGKujbFDRWTXqPIWEYkHwUp6cxMmwKWXmso7NRXWrYOMjIafB2uzU06Bxx5TcCcQhbeISKyz7YaNVjwe83XDBtNVPnt2Qyh/8w106gQ1NeZxsAvd5YK99opsmyWsNNtcRCRWBQJmXNuy4Mcf4eaboXlzuPJKOOwws3573DhYuhRyc81rHnsM9twTzjorqk2X8FLlLSISa4Ld3Q6HCfCVK82a7X79TAV9ySXwxx8mxLOzzVKwzz4zs8jfeUdVdhLQhDURkVhi2w3d3ePGmfXYmZlmydcNN5jn+/SBrl3h/vvNxitTpsDixXDccXDttdFru0SMus1FRGKJZUFxsVna9emncP31MHw45OWZbU3z82HUKLjoIujbt+GP12vGtiUpqNtcRCSatjWL/Oqr4T//gZEj4aij4PHHzUYr8+aZ67t3N5X4Bx80vF7BnVQU3iIi0bL5LPLiYqiqMt/fcIMJ47Vrob4ejjzShPXzz5vxbzDj3I88suU53ZI0FN4iItFiWfD996a6vvFGOPVUKC+Ho482W5t+/LGZmAZwyy0wa1bDY4V2UtOENRGRSAku/QpOSps4EUaPhjvvhN69oUULs6HKq6/C8uVmA5bzzjOzyLOzYdUqc40kPVXeIiLhZttb7kUenE3epg18+CHU1sLhh5uztadMgddeM2u1jzvOVN91deZ6BbdspMpbRCSc/P6GLu5Zs+CZZ2D//eGqqyAtDUpKYOhQ0y1+6KFmc5U5c+Dbb6FpU7OTWqNG0b0HiTlaKiYiEk5Op1nGNWGCmWDWv7+ZSe52w+DBZhb54sUmuH/+2VTctm0mqoGCW7ZJlbeISDh5vXD22WYm+f33m33IJ00yM8cvv9wc2bnffmZXtB9+gKeegjPOiHarJcZpzFtEJFS2roVs2yz5Ou88WL/enPoFcNJJZrz7gw9g9Wr4+mvTbf7LLwpu2SEKbxGR3RXcKMWyzIxyMF+DE9POPRc6djRj3itWmOcuvxzmzzfBnZ1tJqwFDxcR2Q6Ft4jI7th8Qtq4cXDZZeb74MzyYLBfd505svPbb01XeocO8PTTcOaZDSEvsoMU3iIiO8O2TfX8zDPmscMBS5aYjVWmT4eBA7e8Phjs3bubse1XXzV7lAPsu2/Emi2JRRPWRER2VHBzlZUroVMnmD0b2rc3FfTcuTB+vBnDLiuDnBxo1cq8LlidV1WZ13boENXbkPinyltEZEdUVJjlXJ98Ai1bmm7wW281P2vWzJynfcop8K9/wYUXmlnkwQlqweo7J0fBLSGhyltEZHuC25redZfpGp8xwzy/bJlZ4hUImPDOzzfrtJs1M/uVP/UUdO4czZZLglLlLSKyPQ4HVFaaMeuSErNGGxqC2+GA4483J38VF5t13c2aQWFhVJstiUvhLSKyPa+9ZrY0/eEH02X+wAMmzDfndsO778KgQabL/O23IS8vKs2VxKducxGRzW2+9AtMKJ97rjlj+/DD4fff4bbbIDPTbHManMQGsGaNCWyXKypNl+ShyltEBBo2V3E6oaam4SQvyzJV9sqV5vEee0C/fua0r2+/NT8PruVu1kzBLRGh8BYRgYZNVZ5+Gg44AK6+Gl5+GdLTzaSzkhKzBCwlxVybmQn33Wdes3mlLhIB6jYXEQFYutSc+pWebvYinz3bjFuPHw+lpSbUHQ445hh47jm4916zMYsqbYkCHQkqIsln63HtujqYPBleeQXeeQe6djUzxUtLTXX90ktmCdiLL5otTp98Eg45JGrNF1HlLSLJ65lnzOle++5rJppdcgk0aQJjx5qff/ed2YjljDNMN/rmk9NEokhj3iKS+EpLt3w8e7aZOf6//5lJZ927Q2qq2Rntjz/MZDSAdu3g9NP/fNSnSJQpvEUksf3wgxnLDq7LrquDjz6Cxx4zFfaSJWZntMpK0xXevbs5HczjMUd0XnklDBliXquqW2KEwltEElNw+dZ++8HDD8OCBeaxy2W+f/hhM+GsfXv46Sdo3BiaNoVjjzXfL1tmrk9NjU77Rf6GJqyJSGKxbfMnOCHtp5/MSV9PPGHWZw8caJZ5lZbCe++ZiWgAo0aZJWFnnQU9eqjKlpimyltEEkNNDVx/vTma0+Ewx28++ijccYeppE85BSZNgvXroW9fKCgwgT59Ohx3HHz9tZllDgpuiXkKbxGJf5WVZrKZ1wsHHWS2Kc3Ph99+gwkTzFGeRx5pnhs7FgYMgBEjoL7ePL70UhPse+0V7TsR2SHqNheR+OfzmbHsDh3gpJPMhLQ+fUw17XKZ8e/Onc3PXnzRHN957LEm1FVlSxxS5S0i8a9xY5g/34xbn3CCCeTx400X+pw5Zvzb6TRh3aaNWb8NCm6JW9qkRUTi34YNZnnXBx/A8OGmqs7OhpEjYcaMhrAGWLvWzCoXiWMKbxFJHGPHwiefwF13NUw+czhgzJiGtdoiCUDd5iIS17aoP666yoT1lCmwapV5bvp0M/4tkkAU3iISl+rr63n44Yd58cUX8Xq95kmHAwYPhtdfh8WLzXO9e5ttTkUSiGabi0jc+fzzzxk6dCgA48aNw7X5sZwnnGC2QD3iCPNYk9IkAanyFpG4UVpayoABAzj99NO57LLLmDt3Lr179/7zhaefHvnGiUSQwltEYp7X6+XBBx+kY8eOOBwOFi5cyMiRI0nVvuOSpNRtLpIg/AGbKm89VXU+fAEbv20TsMFhgdOySHFY5KSlkONKxemIn67kzz77jKFDh5KSksLEiRM58sgjo90kkahTeIvEqcq6esrcXirc5mttvTlFK5jLm68BDUZ1YOOTmalOGme4yM9IpXGGi9y02KtgS0pKuO666/jkk0+45557GDJkiCptkY0U3iJxxBewKal2s6S8lhqvD4dl4d9qqwb/DuzcUFPvp6beTUm1h4Btk+VKoV1BJntkZ0S9Kvd6vTz66KOMGjWKM844g4ULF9KyZcuotkkk1miTFpE4UOP1sbSiluJKN2DvUEDvLKcFYNE6N4O2+ZlkuSL/u/3UqVO55pprcLlcjB07dtuT0URE4S0Sy7z+AHNXV1Ja7cGyGrq9w8lhmeOwW2Wn06V5Li5n+Oe1Ll++nOuuu45PP/2Ue++9l6uvvpqUFHUMivwVzTYXiVErazxM+W0NK2o82EQmuNn4OTZQuvHzV9V4wvZZdXV1/Otf/6Jz585kZWWxaNEihg0bpuAW2Q79DRGJMZtX29HsFgvY4LVtvimtCEsV/vHHHzNs2DAyMzP55JNP6NWrV8jeWyTRqdtcJIas2+BlZmk5vo3LvGKFw4IUy6JHUQFNGrm2/4K/sWzZMkaMGMG0adMYNWoUgwcPVqUtspPUbS4SI1bVePiqpAxvILaCGzZW4QGbr0rKdrkb3ePxMGrUKDp37kxeXh6LFy/etH5bRHaO/taIxIDSajezV6yPajf5jgjY8E1pBd0L8yjKztjh102aNIlhw4aRk5PDp59+Ss+ePcPYSpHEp8pbJMpW1XjiIriDbGD2ivU7VIH//vvvnH766VxwwQWMHDmSb7/9VsEtEgIKb5EoMmPcFXET3EE2MHNFBes2eLf5c7fbzT333MN+++1Hs2bNWLRoEVdddRVOpzOyDRVJUOo2F4kSrz/AzNJyAtFuyC4K2DCztJw+bZptMQt94sSJXHvtteTn5zNt2jQOPfTQKLZSJDGp8haJkrmrK/HF+WIPn23z4+pKAH777TdOPfVULr74Ym688UZmzZql4BYJE4W3SBSsrPFQWu2JuVnlOytgQ0m1h4fHP8f+++9Py5YtWbRoEYMGDVIXuUgYaZ23SIR5/QGm/LYGb7wn92bcNdXsE6ji0EO6RbspIklBY94iEZYI3eVby8zOxpHVNNrNEEka6jYXiaAary8husu3Fuw+r/H6ot0UkaSg8BaJoKUVtVjRPS47bCzL3J+IhJ/CWyRCfAGb4kp3wlXdQQEbiivd+BP1BkViiMJbJEJKqt0Qd9ux7Cx7432KSDgpvEUiZEl5Lf4Ez26/DYvL1XUuEm4Kb5EIqKyrT5rJXDVeH5V19dFuhkhCU3iLRECZ24sjUWeqbcVhWZS7Fd4i4aTwFomACnc9/gRb2/1X/LZNuXvbB5aISGgovEUioCzJwqzMk1z3KxJpCm+RMPMHbGrr/dFuRkTVev1aMiYSRgpvkTCr8u74+O8PX37ODWf15byD2nL+we0YdtIRTHz52bC17a0xD9GvYyFvjXko5O9dlSQT9ESiQXubi4RZVZ0Ph8V2l4lVr6/ggWsuJzM7l4tuuANnSirLF/9CZdm6yDQ0hBwWVNXVk5+eGu2miCQkhbdImPl2sPt49R/L8Xo8FO3dlm5HH0+TlkVb/Pzey85l6fyf8WyoIaegCT37nszFN92F0+lkzhfTeOXBUaxaXozD6aTlXq0Z9sCT7NmuA0/eMpw5X0yjtqqSrNw8Djz8aK64fRQZWVlbvH+dx82/r76En77+kguvv40zrhjCJ2++zKRXnmNN6R/kN23OUaefTb/B1+JM2f4/HTt63yKy8xTeImHmt+0d2letVdt2FDRrwe+/zGfQ0YdQ0KwFXXodwVmDhlHYug3tD+xKzxNOpd5bx5wvpvHRy8+yxz4dOL7/+bz2yL9YXbKMS265G4DihQvw+0x3/Z7tO9G+S1cCAT8L53zL9AlvU9C8BeePuHnTZ9d53Pxr0EUs+G4mV/3zQY77x/l8OfH/eObumylqsw+X3/ZPZvz3Pd568mGwLPoPue5v78UGAkkyu14kGhTeImG2owVoeqNG/Pudj5j0ynP8+PUXLFu4gM//721+/N8XPPbRdEp//5UJzz5Fvbdu02t+W/AzAEVt2lG8cAHfT/+UvTp0pvuxfWndcV8CgQCr/yhm+oR38GzYsOl1v298XdBHLz+Lr76ea0Y/wVGnnw3ArE8/BuAfVw2n96lnsXen/bnx7BOYOWXSdsMbtj9MICK7ThPWRMLMsYN7s/jq68lv2pwLr7+dh96fwvjPvyMjM4vyNav46OVn+Xryh+zVoRO3jnuZs68aDoDX4wHg2gfGcOOTz7F3p/2YN/N/jBp4PpNeeY6fvv6Cj19/ibwmzbjxyee4/PZRW7wuKK9pMwCmT3gbb92WPwva2T1mnMmxJ41IVKjyFgkzp2WxIzm24vel3H/1JRx24qkUtm5D2aqV1Lk3kNekKXlNTLh6PR7K16xm9saqOOjF0ffQuEVLWrZuQ/nqlSz+8XvWrSylqM0+5nV1dVSWlfHNxxO3+dnHnHkO7tpaPnxxPP+++lJuefpFehx/It98/CHvjnucOo+bLz78PwB69Dlpu/diQdLsKCcSDQpvkTBL2cHSO7dxE9odcCBfT/qA9evWkuJy0anroVxw/W3s1aETP3w5jblfTefDF8dz6PEnsXzJwi1eP/nV51m/bi1pGY3odvTxnHbpYPKaNuP4/hfw5cT3eW/84/TpfyELvpu5zc+/5Oa78GyoZerbrzJ66OXc9ORzXHn3v5n0ynM8d98d5DVpRv8h19Fv0LCQ3reI7DzLtjWrRCScKjxepi8rS/jDQDdnAUft1URLxUTCRGPeImGW40rOAMtxqWNPJFwU3iJh5nRYZKY6o92MiMp0OXGq21wkbBTeIhHQOMMV7SZEVOP05LpfkUhTeItEQH5GKs4kmX3ttCwKkuyXFZFIU3iLREDjDFfS7DgWsG0KMpJznF8kUhTeIhGQm5ZKVpJM4MpypZCbpvAWCSeFt0iEtCvITPhdx5wWtC/IjHYzRBKewlskQlplZ8AO7bUWz6yN9yki4aTwFomQFIdF69yMHd7rPN44LGidm6ElYiIRoPAWiaC2+Zkk6rw12zb3JyLhp/AWiaAFc+fwy6wvIRCIdlNCymFBq+z0pJmUJxJtCm+RCFi3bh0DBw7k6KOPxl+ylNSUxNpxLcWy6NI8N9rNEEkaCm+RMPL7/YwbN4727duzZs0a5s2bxx233kK3lnkJM3XNArq1zMPl1D8nIpGiPi6RMJk5cyZDhgyhsrKSV199lZNOajgHu2VWOkXZ6ayo8RCI4zFwhwVFWem0yEqPdlNEkop+VRYJsbVr13L55Zdz7LHHcuaZZzJv3rwtgjvowOa5pMT5lqnqLheJDoW3SIj4/X7Gjh1L+/btKS8vZ/78+dx+++2kp2+7KnU5HfQoKojbpWMOC3oUFai7XCQKLNtO1IUrIpHz9ddfM2TIEGpqahgzZgwnnHDCDr92VY2Hb0oriKe/iBbQsyhf3eUiUaJfmUV2w5o1a7j00ks5/vjjOfvss5k3b95OBTdAi6x0uhfGzwQ2C+hemKfgFokihbfILvD5fIwZM4b27dtTVVXFggULuO2220hLS9ul9yvKzqBnUX7Md6E7LOjZKp8ibYEqElXqNhfZSV999RVDhgzB4/EwZswY+vTpE7L3XrfBy8zScny2HVOz0B2WmZzWo6iAJo10VrdItKnyFtnc3/wu6/f7ueKKKzjhhBMYMGAAP/30U0iDG6BJIxd92jSjMCs9ZrrRLcxysD5tmim4RWKEKm8RMKFt2+BwNDzeahmX2+1m8uTJHHLIIeyxxx5hb9LKGg/fr1wftSo8WG13a6nxbZFYo/AW2dwff8Djj0OfPuZPlHn9AX5cXUlJtQfLIiIh7rDM7y6tstPp0jxXS8FEYpDCWyTogQfg9dfh/PNh8GDIzjbPb6MKj7Qar4+lFbUUV7oBG38Y/tY6LQCLvfMyaJOXqUNGRGKYwluSz9Zd5ACVlXDFFXDbbXDggbBmDaxdC/vuG7Vmbos/YFNS7WZxeS01Xh8Oy8K/G3+FnZZFwLbJcqXQviCTVtk6j1skHii8JblsXkUvXw7FxdCunXk8fDgsWwbdu8PKlTBrFgwbBtdfHxPV99Yq6+opd9dT7vZS5vZSW+8H2LTcbPO/2MGWB7vdM11OGqe7KMhwUZCRSm5aasTaLSK7T+EtyWHr8L37bnj/fejXD8aPh88/B68X5s+Hbt2gaVP48EP44Qd4+OGoNXtn+AM2VV4fVXX1+AI2Adt0rzstcFgWKQ6LnLRUclwpqq5F4pwGtSTx+f3g3Oz87M8+gw0bYM4c+OYb+Pe/zfcDBsD++5trnn4anngCHnkkOm3eBU6HRX56KvnpqqJFEp3CWxJTTQ1MnQpnnmnGtletgjfegMsvN0G9fr2ZlPb77ybMDzsMAgHTjX7TTVBfDxMnQtu20b4TEZE/0RoQSUwzZsCQIbBwoekuf/NNM46dkwOFhfDxx9CpU0Nwz5kDjz4KbdrAqFEwYYKCW0RilsJbEsfq1XDjjSakDz8c+veHO+80P3vvvYZ120VFcPzxsGiRmWU+ejRceGHD7PMOHaLTfhGRHaQJaxL/amqgqgoKCmDFCmjVClwu+Okn001+/vlm85Vbb4XGjc1rfv/dzCIPBvaDD0Lr1lG7BRGRnaExb4lv5eVw112QkmJmhTdqBGecARdcAOedZ77ecYd5ft06yMuDHj2gZUtTjVdXN2zGIiISJ9RtLvGtoAC6djUT0KZONZX1YYfBf/9rqvEBA+CUU+Ckk+Dee02X+aRJZiwcFNwiEpcU3hK/giM+p5wCzZvDBx9Aba1Zu+1wmPXbzZrBueeaddw+nxkTf+EFM9NcRCROKbwlfmw9PSO46UqTJnDccWbs+913zSzyE0+EL7+EefPgtNPMuHdlpXmPFI0WiUh804Q1iX3b2os8+DyYEPf7zVKvn382E9OaNIHrroOsLBg7NvJtFhEJI4W3xI9Fi8yWpu3bm0lpwV3TgjuozZ9vdkbLzDTLv376yYxxB2eYi4gkCHWbS3x46CE45xwzbn333XDNNQ2TzoIV+b77mu1NfT4oK4MDDlBwi0hCUuUtscW2zTalm+9FXl8Pl1wCQ4dCz57w669m5njv3uYYT8tqOHjE7YaMjKg1X0QkElR5S2xYt86cn21ZJriD52mD2VDlyy9hn33M4332MVuXvv9+w6S14FcFt4gkAYW3xIb+/eG++0yX94gRcPTRcOqp8N13Zoy7bduGrU7BnLndtu2fZ6CLiCQBrZmR6Kqvh9RU+Ne/zP7izZqZpVzz55vZ4nfeCS+/DM89Z87Zbt7cHOc5YYLZ0tTSudQiknxUeUt0paaag0R69DAT0p5+Go45xvzsgQfA44GXXjKnfU2caE4E83rNqWGnnx7dtouIRIkqb4muFSvg9tvhoovM+ux33zXj34GAqcBHjDB7l/fqZbY9PeywaLdYRCTqVHlLZGw9Nv3ee7Bkiamka2rMDPJGjcwZ3I8/bkIdzLj3/vubbU9FRATQUjGJhg0bzPrrXr3Mki+3G4YNM+PcYCarHXQQ/Pvf5mjPQODPu6uJiCQx/YsokWHb8Pbb5vCQRo1g4EDo29dU2n4/HHKIWQ4GZhOWefOgrs48VnCLiGxB/ypK6C1ZYqppaOguLyuD0lK4/nrz1eWCvfeGf/7TzDSfOdNU5ABHHglTpui4ThGRv6DwltCaMQNuucXMDIctT/4aMQLOPNNMTNtzTxgzxhznefPNZkOWX3+NXrtFROKIxrwlNHy+hqM2b77ZVNxXXw177WXGrC2rIcjvuQcWL4avvzYhv+++JrybNo1e+0VE4ogqbwmNYHD/3/9BcbEJ5s8/N885HCa4AwHzeMQIU3GvWNHwnIJbRGSHqfKW0HC74fLLobzcdJvffju0agXDh8OhhzYcHLK52lpzfKeIiOwUVd6y8/z+Pz+3YYPZDW38eDPh7LnnIC8Ppk41s8aDJ39tTsEtIrJLFN6y44LhGzyuc/p0M3YNsHy5OV+7USOzX3n79pCTA2+8AR99ZK7RPuQiIiGh8JYdFwzf4mKz//j118MZZ8CkSWZTlSZNzMYqQS1bmq5zn0+nf4mIhJDGvOXvBXc38/vN18ceg99+gwMPNGPc99xj1nXfc48J93/8w2xnuny56RYfPRo6d472XYiIJBRV3rJttm3+BHc3czpNOBcXm0q7sNA8P3y4Cfa33oLWrc3Sr7POgosvhg8/VHCLiISBThWTbQt2kc+eDWPHwsEHw7HHmt3Qfv4ZVq2C6mrIzYUBA8xhIvvua47pPO206LZdRCTBqfKWBj/8YCabgekuHzUKRo4052zPmweDB5uztM8/H95/HxYtMteedhqccAJ06RK9touIJBGFtxivvw6//AKpqeaxbZsAf/dd02X+9ddw9tmQn2/GutPS4NVXYc0ac/0NN5hucxERCTtNWEtmtm3Gq4O7o61dC59+aqroZctg6FDzfLNmZhZ5+/awbp1Zv/3NN+bPyJENS8dERCQiNOadjIK7nVmWCe6aGjO5zOMxY9eFhWajlfp60xX+zDPmdd99Z04BGzkSjjgCeveO7n2IiCQpdZsnmzffhJ49Yf5883jSJLjwQvj+e7j0Ujj8cNMd7vWaanvWLPP8oEENZ3AfcUR070FEJMmp2zyZzJ8PF11kzspOS4OvvoKbbjJV9hNPmGuWLjVhfeWVcMEFZmb5ggVQUmKe0xnbIiJRp27zZLLvvmad9hVXmN3QLrvMVNFz5jR0pbdta87cfvVVc/1BB5lNV0REJGYovJNFIGD2Hvd4YMYMc/oXmO7x1avh2WdNtzjANdeY2eUOjaqIiMQi/eucLBwO2GcfU2Xvvz/ccYd5/oAD4KijzOEhxcXmuZQUs2Oa1m2LiMQkhXei2dZxnUEuF3ToAHfdBU8+aZaD5eebCWxpaTB5csO1qrpFRGKWJqwliuD/xuC2pgsWmE1TGjVqOFxkcwMHmkp76lQT+H/8oU1WRETihMI70cyYAbffbtZqr10LTz0FHTv+OcBLSsyEtC++UPe4iEicUXgnkvfeg//8xxzP2bq1mSnevj1Mn77t69etM7PORUQkrmhgMx791e9bBxxgNmFZsgT69IE77zTd5+PHm58HAlter+AWEYlLCu944/c3jGvDloHcrh2sXw8TJpgDRQYPNhuwXHUVVFZqEpqISILQv+bxIlhtO52mu/vRR83jrQP5119h4kQT5HPmmGr8H/8wZ2+LiEhCUHjHi2C1/dBDcNxxZna42/3nLvTjjoODD4ZjjoGTT4auXc2a7VatIt9mEREJC+2wFqts2/zZvLL+8ENzkMi33zacu+3xQHq6+d7vN5X5xx+b3dQOPNCs7RYRkYSi8I5FwRC2rC3DOSXFPB41yvzsu+/MsZ3vvmsODAmeq52TA927R6/9IiISVuo2j0XBEH7wQTjhBLj/fvjpJzjxRDjlFDNJrWtX8/M1a8xxniIikjRUeceiRYtgxAhzwtfYsWbTlcmTzYYqt97acN1//2t2UGvcOHptFRGRiFPlHW0+35+f83hg5EhTcT/2GNTWwqpVcN995uf/+585yvO558wOajqyU0QkqWiHtWh58UXIzDTLuIKPW7Qw3eQAVVVmfXbnznDbbeYwkccfh8WLTeAvWGBmlouISNJRt3m0VFXB0KHQrRsMH24e19TA22/D88+btdxffQWvvWau9/vNlqdTpsAFF5i9y0VEJCnFZeXtD9hUeeupqvPhC9j4bZuADQ4LnJZFisMiJy2FHFcqToe1/TeMlp49zeSz4cNhwAAoLTX7kb/xBhx7rNkdLSfHTErbf38zQS0/P9qtFhGRKIuL8K6sq6fM7aXCbb7W1pszq4O5vPkNBKM6sPHJzFQnjTNc5Gek0jjDRW5aasTavV0LF5pu8TfegHPOMc/dfTdMmwaffw5lZfD662aDlbPPjmpTRUQkdsRsePsCNiXVbpaU11Lj9eGwLPy70VSnZRGwbbJcKbQryGSP7IzYqMqvuMJsabr5yV9Nm8JNN8H110etWSIiErtiLrxrvD6WVtRSXOkGbPxhaJ3TArBonZtB2/xMslyRHfoPBAI4gjun2Tbk5ZmjPPv3N8/NnGkCvG3biLZLRETiQ8yEt9cfYO7qSkqrPVhWQ7d3ODksk52tstPp0jwXlzO8K+dWrVrFjTfeSL9+/Tj55JNJSdn4S8O4cXD11WYWuU7+EhGR7YiJpFhZ42HKb2tYUePBJjLBzcbPsYHSjZ+/qsYTls/x+Xw89thjdOjQAY/HwyGHHNIQ3GCO7nz0UQW3iIjskKhW3ptX27FQ/luEvgr/4osvGDJkCD6fjzFjxnCc1maLiMhuilqpt26Dd4tqOxZsXoWv2+DdrfdauXIlF1xwASeffDIXXXQRP/74o4JbRERCIirhvarGw1clZXgDdsS6yHdUwAZvwOarkrJd6kavr6/nkUceoUOHDvj9fhYuXMgNN9yAS0dziohIiES827y02s3sFetjptr+OxbQvTCPouyMP/3Mtm1mz55N9+7dsSyz5Gz69OkMHToU27Z58sknOfrooyPcYhERSQYRrbxX1XjiJrjBdKPPXrF+mxX4m2++SY8ePXj33XcpLS1lwIABnHbaaVx66aXMnTtXwS0iImETscp73QYvX/1RRiASHxZiDgsOb9WYJo1M13dZWRlt2rShqqqKzMxMLMvitNNO48EHH6RQe46LiEiYRaTy9voDzCwtj8vgBjMOPrO0HK/f3MGwYcNwu90AbNiwgZNPPpnXXntNwS0iIhERkcp79ooKVtR4Ym5y2s5wWFCUlc5vX07h3HPP3eJnTqeTxYsX06ZNmyi1TkREkknY9wVdWeOJmXXcuyNgQ0m1h/9+NgOXy0WLFi1o1qwZTZs2paioiPT09Gg3UUREkkRYK2+vP8CU39bgjeeSeysuh0WfNs3CvpWqiIjIXwlrAs1dXYkvNrZODxmfbfPj6spoN0NERJJY2MK7xuujtDq+x7m3Jdh9XuP1RbspIiKSpMIW3ksrarFi4LjscLAsc38iIiLREJbw9gVsiivdCVd1BwVsKK5040/UGxQRkZgWlvAuqXZD3M8v3x57432KiIhEVljCe0l5Lf4Ez26/DYvL1XUuIiKRF/LwrqyrT5rJXDVeH5V19dFuhoiIJJmQh3eZ24sjUWeqbcVhWZS7Fd4iIhJZIQ/vCnc9/gRb2/1X/LZNudsb7WaIiEiSCUvlnUzKPMl1vyIiEn0hDW9/wKa23h/Kt4x5tV6/loyJiEhEhfRgkirv7o3/9uvYcKRmSmoqTQpb0ffcizjt0kFb/Py9hSt263NCrcrrIz89NdrNEBGRJBHa8K7z4bDY7WVi14x+Aq/bzVtPPsRLo++hcYuW9DrxNEY8/FRoGhpCDguq6uoV3iIiEjEhDW9fiLqPDzvhFFxp6axc/jsfPD+OX76bRa8TT+PRkVcDcPjJZ/DWmId4e+wjW7xu30N6MvT+x7jquEP/9J5PfzqL8jUrefrOG1lb+gcALffam3OH3cAhx/TdrfaG6r5FRER2REjD22/bIdlXrXp9BV6Ph3kz/wdAs6I9/nRNz76nUNRmH2zb5u0nH2ZF8W90OKgbOQWNN1XoP8/8ik/feZ2We7UhO7+Amqr1HH3GP8jKy6eqvIzJr73IoyOv5j8z5pCZk7tLbbWBQJLMrhcRkdgQ0vAOVQF65ZFdN33fpdeR9D3v4j9ds2f7juzZviMv3H8XK4p/45izzuG8ETdjWRaHn3wGC+d8y1cfTaBJYRF3vfAmGZmZeD0eZvz3Pf74dRGbH2Ne+vtS2nc5eJfbm+i7yYmISGwJaXg7QrQ3y23PvEpaRiOaFraiWVGrv7zuzSceZOJL/+GwE09l8D8fwtq4OcxvC37mvkEXkp6ZxV3Pv0XTQvMeLz9wL8uXLOSMK67mgJ69ef2x0fz681y8Hs9utdeZHHvSiIhIjAhpeDsti1Dk2H6HHoYrLf1vr5ny1qu889Sj5DVtRtejjuebjz8kt3ETClu34Z9XnMeG6ipOPP9Sfpv/E7/N/4luR/fZ9NqaykqWzv+J4oULdrutFiTNjnIiIhIbQhreKaEqvXfAoh++A2D92jWMuWkYYCas9R86kqryMgDeG/f4puuf/nQWF990F0/eOoIvPniPrkcdx/49evHDl5/vdlsied8iIiKWbYdutlWFx8v0ZWUJfxjo5izgqL2aaKmYiIhETEh3WMtxJWeA5bhC2oEhIiLyt0Ia3k6HRWaqM5RvGfMyXU6c6jYXEZEICvnBJI0zXKF+y5jWOD257ldERKIv5OGdn5GKM0lmXzsti4Ik+2VFRESiLyyVd7LsOBawbQoyknOcX0REoifk4Z2blkpWkkzgynKlkJum8BYRkcgKeXgDtCvITPhdx5wWtC/IjHYzREQkCYUlvFtlZ0BI9lqLZdbG+xQREYmssIR3isOidW5GyPY6jzUOC1rnZmiJmIiIREVYwhugbX4miTpvzbbN/YmIiERD2MI7y5VCq+z0hKu+HRa0yk5Pmkl5IiISe8IW3gBdmueSkmBrvlMsiy7Nc6PdDBERSWJhDW+X00HXlnkJM3XNArq1zMPlDOt/NhERkb8V9hRqmZVOUQJ0nwe7y1tk/f054yIiIuEWkRLywAToPld3uYiIxIqIhLfL6aBHUUHcVt8OC3oUFai7XEREYkLE0qhJIxc9CvPjbvzbAnoU5tOkkQ4gERGR2BDRUrJFVjrdC+NnApsFdC/M0zi3iIjEFMu2I7+VyqoaDzNXVBCI4U1cTFd5Pi0yFdwiIhJbohLeAOs2eJlZWo7PtmMqxB2WmZzWo6hAXeUiIhKTohbeAF5/gLmrKymt9hAL+W1hloN1aZ6ryWkiIhKzohreQStrPHy/cn3UqvBgtd2tpca3RUQk9sVEeIOpwn9cXUlJtQfLIiIh7rDMISOqtkVEJJ7ETHgH1Xh9LK2opbjSDdj4w9A6pwVgsXdeBm3yMnXIiIiIxJWYC+8gf8CmpNrN4vJaarw+HJaFfzea6rQsArZNliuF9gWZtMrWedwiIhKfYja8N1dZV0+5u55yt5cyt5faej/Aph3bNr+BYBwHu90zXU4ap7soyHBRkJFKblpqxNotIiISDnER3lvzB2yqvD6q6urxBWwCtuled1rgsCxSHBY5aankuFJUXYuISMKJy/AWERFJZppeLSIiEmcU3iIiInFG4S0iIhJnFN4iIiJxRuEtIiISZxTeIiIicUbhLSIiEmcU3iIiInFG4S0iIhJnFN4iIiJxRuEtIiISZxTeIiIicUbhLSIiEmcU3iIiInFG4S0iIhJnFN4iIiJx5v8B6YraZWP/MmEAAAAASUVORK5CYII=",
      "text/plain": [
       "<Figure size 480x320 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import networkx as nx\n",
    "\n",
    "# Fetch records\n",
    "records = recall_vector_store.similarity_search(\n",
    "    \"Sasako\", k=2, filter=lambda doc: doc.metadata[\"user_id\"] == \"3\"\n",
    ")\n",
    "\n",
    "# Plot graph\n",
    "plt.figure(figsize=(6, 4), dpi=80)\n",
    "G = nx.DiGraph()\n",
    "\n",
    "for record in records:\n",
    "    G.add_edge(\n",
    "        record.metadata[\"subject\"],\n",
    "        record.metadata[\"object_\"],\n",
    "        label=record.metadata[\"predicate\"],\n",
    "    )\n",
    "\n",
    "pos = nx.spring_layout(G)\n",
    "nx.draw(\n",
    "    G,\n",
    "    pos,\n",
    "    with_labels=True,\n",
    "    node_size=3000,\n",
    "    node_color=\"lightblue\",\n",
    "    font_size=10,\n",
    "    font_weight=\"bold\",\n",
    "    arrows=True,\n",
    ")\n",
    "edge_labels = nx.get_edge_attributes(G, \"label\")\n",
    "nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_color=\"red\")\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-opentutorial-VHYpHY_j-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
